#! /usr/bin/env python
#
# Copyright (C) 2010, Giuseppe Di Martino; All Rights Reserved.
#
# This software is distributable under the terms of the GNU
# General Public License (GPL) v2, the text of which can be found at
# http://www.gnu.org/copyleft/gpl.html. Installing, importing or otherwise
# using this module constitutes acceptance of the terms of this License.
#
# Disclaimer
#
# This software is provided "as-is".  There are no expressed or implied
# warranties of any kind, including, but not limited to, the warranties
# of merchantability and fittness for a given application.  In no event
# shall Giuseppe Di Martino be liable for any direct, indirect, incidental,
# special, exemplary or consequential damages (including, but not limited
# to, loss of use, data or profits, or business interruption) however
# caused and on any theory of liability, whether in contract, strict
# liability or tort (including negligence or otherwise) arising in any way
# out of the use of this software, even if advised of the possibility of
# such damage.
#
# Comments and/or additions are welcome (send e-mail to: gius.dima@gmail.com).
#

"""
pyhidecode.py module

######################################################
#######  Written by:  Giuseppe Di Martino  ###########
#######     Last modified: Mar 28, 2010    ###########
######################################################

This module can be used as a standalone application or imported from another 
module, in the latter case it provides the convert_file(...) function.
The functionality exposed by this module is to transform Python source code 
by replacing a placeholder present in the original source code with some code
snippets, choosed from a prefixed list.
The number of code snippets is random within a maximum value, and each code
snippet consists of harmful python code, so to hide the original source code
within the added harmful code.
This makes very complicated for an hacker, who disassembles a program written
in Python to obtain the original source code.
"""

## CHANGE LOG:
## ==========
## 10-03-28 ... first draw and public release

import sys
import os
import random
from optparse import OptionParser
import py_compile



__version__ = 0.1



# placeholder to insert into the source code that will be replaced.
# The resulting placeholder string is ## HideCode ##
HIDECODE_WORD = 'HideCode'.upper()
HIDECODE_PHRASE = '## %s ##' % HIDECODE_WORD

# in source code each placeholder will be replaced by a random number of
# code snippets. The random number is less or equal to MAX_INSERTED_SNIPPETS
MAX_INSERTED_SNIPPETS = 30

# list of default code snippets. This list can be replaced by an external
# module indicated from commandline (when used as a standalone program) or as 
# a parameter to the convert_file(...) function
CODE_SNIPPETS = [
"""
str(3) + 'user names' + ' and ' + str(2*3) + ' passwords'
""",
"""
10*('This text' + 'is joined ' + str(10) + 'times')
""",
"""
'x'.join('text containing the password'.upper().split())
""",
"""
(lambda x: x**2 + 3*x)(5)
""",
"""
(lambda x, y: x*y + x*x + y*y)(5, 3)
""", 
"""
'top secret user profile, do not public !'
""",
"""
'you have discovered a very deep secret, the hot water !'
""",
"""
'Warning: this is a tremendous virus ! Please stay away from here'
""",
"""
'print "All rights reserverd for everyone"'
""",
"""
'print 10**1000'
""",
]



def insert_codesnippet(dest_file, indent_space, max_snippets, 
                         strip_blank_lines, code_snippets):
    """Writes a random number of code snippets to dest_file.
dest_file: destination file, previously opened in write mode
indent_space: string containing white spaces representing code's indentation
max_snippets: maximum number of code snippets to write
strip_blank_lines: boolean parameter stating if blank lines contained in code
                   snippets must be stripped away.
code_snippets: list of available code snippets to be choosed from. If this list
               is empty, no code is written to the file.
"""
    if not code_snippets:
        return
    for n in xrange(random.randint(1, max_snippets)):
        snippet = random.choice(code_snippets)
        for line in snippet.split('\n'):
            if line or not strip_blank_lines:
                dest_file.write(indent_space + line + '\n')

        

def convert_file(source_path, dest_path, compiled=True,
                   max_snippets=MAX_INSERTED_SNIPPETS, verbose=True, 
                   strip_blank_lines=True, code_snippets=None):
    """Convert source_path file in a new file created in the folder dest_path
where each placeholder is replaced with a random number of harmless python code.

source_path: original python source code to be converted
dest_path: destination folder where the converted file is created
compiled: if True a .pyc/.pyo file will be created, else the created file 
          will be a .py
max_snipped: see insert_codesnippet(...)
verbose: if True status message will be written to stdout
strip_blank_lines: see insert_codesnippet(...)
code_snippets: see insert_codesnippet(...)
"""
    if verbose:
        real_dest_path = dest_path
        if compiled:
            if __debug__:
                real_dest_path += 'c'
            else:
                real_dest_path += 'o'
        print source_path, '-->', real_dest_path, '...',

    if code_snippets is None:
        code_snippets = CODE_SNIPPETS
        
    # never overwrite the source file !
    if (os.path.exists(dest_path) and 
        os.path.samefile(source_path, dest_path)):
        print 'source file and its destination are the same file'
        return

    count_inserted = 0
    count_warning = 0
    source_file = open(source_path)
    dest_file = open(dest_path, 'w')
    for index, line in enumerate(source_file):
        upper_line = line.strip().upper()
        if upper_line == HIDECODE_PHRASE:
            # indent_space = text (spaces) on the left to HIDECODE_PHRASE in line
            indent_space = line.rstrip()[:-len(HIDECODE_PHRASE)]
            insert_codesnippet(dest_file, indent_space, max_snippets, 
                               strip_blank_lines, code_snippets)
            count_inserted += 1
        else:
            dest_file.write(line)
            if HIDECODE_PHRASE in upper_line:
                print '\n*** Warning: %s in %s at line %d' % (HIDECODE_PHRASE, 
                                                               source_path, 
                                                               index + 1)
                print line
                count_warning += 1
            elif HIDECODE_WORD in upper_line:
                print '\n*** Warning: %s in %s at line %d' % (HIDECODE_WORD, 
                                                              source_path, 
                                                              index + 1)
                print line
                count_warning += 1
        
    dest_file.close()
    source_file.close()
    
    if verbose:
        print '%d found, %d warnings.' % (count_inserted, count_warning)

    if not compiled:
        print
    else:
        if verbose:
            print 'Compiling...',
        py_compile.compile(dest_path, doraise=True)
        os.remove(dest_path)
        if verbose:
            print 'done'
        

    
def main():
    """Entry point if this module is run as standalone
"""
    print 'PyHideCode %s - Copyright (C) 2010 by Giuseppe Di Martino.' % __version__

    parser = OptionParser('Usage: %prog [options] <source_filename.py> <dest_folder>')
    parser.add_option("-q", "--quiet", dest="verbose", default=True, 
            action="store_false", 
            help="don't print status messages to stdout")
    parser.add_option("-b", "--no-strip-blank", dest="strip_blank_lines", 
            default=True, action="store_false", 
            help="don't strip blank lines from code snippets")
    parser.add_option("-s", "--max-snippets", dest="max_snippets", 
            type="int", default=MAX_INSERTED_SNIPPETS, 
            help="maximum number of code snippets inserted for each %s found" % HIDECODE_PHRASE)
    parser.add_option("-o", "--overwrite-dest", dest="overwrite", 
            default=False, action="store_true", 
            help="overwrite destination file")
    parser.add_option("-c", "--no-compiled", dest="compiled", default=True,
            action="store_false", 
            help="disable compiler, destination file will contains Python source code")
    parser.add_option("-m", "--codesnippets-module", dest="codesnippets_module", 
            type="string", default='', 
            help="custom Python module containting codesnippets to be used")
    parser.add_option("-r", "--remove", dest="remove", default=False,
            action="store_true",
            help="removes all %s without code snippets insertion" % HIDECODE_PHRASE)
    (options, args) = parser.parse_args()
    
    if len(args) != 2:
        parser.print_usage()
        return
    
    source_path = args[0]
    if not os.path.exists(source_path):
        print source_path, 'does not exists'
        return
    
    dest_folder = args[1]
    if dest_folder and not os.path.isdir(dest_folder):
        print dest_folder, 'must be a folder'
        return
    
    source_basename = os.path.basename(source_path)
    dest_path = os.path.join(dest_folder, source_basename)

    if os.path.exists(dest_path) and not options.overwrite:
        print dest_path, 'exists'
        return

    code_snippets = CODE_SNIPPETS
    if options.codesnippets_module: # import snippets from an external module ?
        snippet_filename = options.codesnippets_module
        # strip extension from filename to import
        for ext in ('.PY', '.PYC', 'PYO'):
            if snippet_filename.upper().endswith(ext):
                snippet_filename = snippet_filename[:-len(ext)]
                break
        # try to import the external module
        try:
            codesnippets_module = __import__(snippet_filename)
            code_snippets = codesnippets_module.CODE_SNIPPETS
            if options.verbose:
                print 'using code snippets from', snippet_filename
        except ImportError:
            print "Unable to import", snippet_filename
            return
        
    if options.remove:
        code_snippets = []

    convert_file(source_path, dest_path,
                 compiled=options.compiled,
                 max_snippets=options.max_snippets, 
                 verbose=options.verbose,
                 strip_blank_lines=options.strip_blank_lines, 
                 code_snippets=code_snippets)
        
    
    
if __name__ == '__main__':
    main()
